﻿
#region Using Statements

using System;
using System.Collections.Generic;

using Microsoft.Xna.Framework;

using Freeform;
using Microsoft.Xna.Framework.Graphics;

#endregion


namespace Freeform
{
    public class Vertices : Entity3D
    {

        #region Members

        //This contains data for the position, color and normal for each vertice. Normal(s) are used for the crappy lighting.
        public VertexPositionColorNormal[] vertices;

        //This is an array of all indices. It's in a short because we're running in the Reach profile apparently and it can only handle
        //16 bit indice buffers. The alternative is the Hi-Def profile which only works on win/x360 but can support 32-bit indices.
        public short[] indices;

        //These are shorts to prevent conversion errors.
        public short terrainWidth = 4;
        public short terrainHeight = 3;
        
        //Dimensional array for the Z positions of each indice as derived from a heightmap.
        public float[,] heightData;

        //This is the view code stolen from Actor3D_Model. Necessary to articulate the effect into view.
        public Matrix WorldM { get; set; }
        public Matrix ProjM { get; set; }
        public Matrix ViewM { get; set; }

        public Vector3 OrientationOffset { get; set; }
        public Vector3 ScaleOffset { get; set; }
        public Vector3 PositionOffset { get; set; }

        //This is the shader that does all the work.
        public Effect TerrainEffect { get; set; }

        #endregion


        #region Initialisation

        public Vertices(World World, Viewport3D View)
            : base(World)
        {
            Position = new Vector3(0, 0, -25);
            OrientationOffset = new Vector3(1.5708f, 0, 3.14159f);
            ScaleOffset = Vector3.One;
            TerrainEffect = Game.Content.Load<Effect>("Effects/effects");
            Texture2D heightMap = World.Game.Content.Load<Texture2D>("Textures/seamless");
            //Order of these four methods is important. Loadheight then set up the verts and indices, calucate normals last.
            LoadHeightData(heightMap);
            SetUpVertices();
            SetUpIndices();
            CalculateNormals();
        }
        #endregion


        public struct VertexPositionColorNormal
        {
            //XNA does not include a type for storing Position/colour/normal.
            //It has a Position/Colour type but we needed to make a new one for the normal.
            //Is used inside this class and by the DrawUserPrimitives/DrawIndexedPrimitives draw calls.
            public Vector3 Position;
            public Color Color;
            public Vector3 Normal;

            public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration
            (
                new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
                new VertexElement(sizeof(float) * 3, VertexElementFormat.Color, VertexElementUsage.Color, 0),
                new VertexElement(sizeof(float) * 3 + 4, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0)
            );
        }

        #region Update
        public override void Update(GameTime Time)
        {
            base.Update(Time);

        }
        #endregion
        public override void Draw(GameTime Time, Viewport Viewport)
        {
            base.Draw(Time, Viewport);

            Viewport3D View = Viewport as Viewport3D;

            // Calculate absolute geometrics
            Vector3 AbsOrient = Orientation + OrientationOffset;
            Vector3 AbsScale = Size * ScaleOffset;
            Vector3 AbsPosition = Position + PositionOffset;

            // Create world matrix.
            WorldM =
                Matrix.CreateRotationX(AbsOrient.X) *
                Matrix.CreateRotationY(AbsOrient.Y) *
                Matrix.CreateRotationZ(AbsOrient.Z) *
                Matrix.CreateScale(AbsScale) *
                Matrix.CreateTranslation(AbsPosition);

            // Create view matrix.
            ViewM = Matrix.CreateLookAt(
                View.Camera.Position,
                View.Camera.LookAt,
                View.Camera.Orientation);

            // Create projection matrix.
            ProjM = Matrix.CreatePerspectiveFieldOfView(
                View.Camera.FieldOfView,
                View.Camera.AspectRatio,
                View.Camera.NearPlane,
                View.Camera.FarPlane);

            // Setting up the GraphicsDevice to use LinearClamp. It doesn't like values that are non-powers of two without
            // Linear Clamp iirc. But Linear Clamp fucks up drawing the regular models so it's reset again in the Viewport3D draw calls.

            SamplerState TerrainSamplerState = new SamplerState();
            TerrainSamplerState = SamplerState.LinearClamp;
            Game.GraphicsDevice.SamplerStates[0] = TerrainSamplerState;


            // Sets all the values required for drawing based on the effect called "effects.fx".
            TerrainEffect.CurrentTechnique = TerrainEffect.Techniques["Colored"];
            TerrainEffect.Parameters["xEnableLighting"].SetValue(true);
            Vector3 lightDirection = new Vector3(1.0f, -1.0f, -1.0f);
            lightDirection.Normalize();
            TerrainEffect.Parameters["xLightDirection"].SetValue(lightDirection);
            TerrainEffect.Parameters["xAmbient"].SetValue(0.1f);

            TerrainEffect.Parameters["xView"].SetValue(ViewM);
            TerrainEffect.Parameters["xProjection"].SetValue(ProjM);
            TerrainEffect.Parameters["xWorld"].SetValue(WorldM);

            //Execute Effect passes and draw the terrain.
            foreach (EffectPass pass in TerrainEffect.CurrentTechnique.Passes)
            {
                pass.Apply();
                Game.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length, indices, 0, indices.Length / 3, Freeform.Vertices.VertexPositionColorNormal.VertexDeclaration);
            }

            SamplerState ModelSamplerState = new SamplerState();
            ModelSamplerState = SamplerState.LinearWrap;
            Game.GraphicsDevice.SamplerStates[0] = ModelSamplerState;
        }

        private void SetUpVertices()
        {
            float minHeight = float.MaxValue;
            float maxHeight = float.MinValue;

            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainHeight; y++)
                {
                    if (heightData[x, y] < minHeight)
                        minHeight = heightData[x, y];
                    if (heightData[x, y] > maxHeight)
                        maxHeight = heightData[x, y];
                }
            }

            if (terrainWidth * terrainHeight > 32767)
            {
                vertices = new VertexPositionColorNormal[32767];
                terrainWidth = 181;
                terrainHeight = 181;
            }
            else
                vertices = new VertexPositionColorNormal[terrainWidth * terrainHeight];

            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainHeight; y++)
                {
                    vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y);

                    //Sets color based on height from ground. To "simulate" lakes/mountains.
                    if (heightData[x, y] < minHeight + (maxHeight - minHeight) / 4)
                        vertices[x + y * terrainWidth].Color = Color.Blue;
                    else if (heightData[x, y] < minHeight + (maxHeight - minHeight) * 2 / 4)
                        vertices[x + y * terrainWidth].Color = Color.Green;
                    else if (heightData[x, y] < minHeight + (maxHeight - minHeight) * 3 / 4)
                        vertices[x + y * terrainWidth].Color = Color.Brown;
                    else
                        vertices[x + y * terrainWidth].Color = Color.White;
                }
            }

        }

        private void SetUpIndices()
        {
            indices = new short[(terrainWidth - 1) * (terrainHeight - 1) * 6];
            int counter = 0;
            for (int y = 0; y < terrainHeight - 1; y++)
            {
                for (int x = 0; x < terrainWidth - 1; x++)
                {
                    short lowerLeft = (short)(x + y * terrainWidth);
                    short lowerRight = (short)((x + 1) + y * terrainWidth);
                    short topLeft = (short)(x + (y + 1) * terrainWidth);
                    short topRight = (short)((x + 1) + (y + 1) * terrainWidth);

                    indices[counter++] = topLeft;
                    indices[counter++] = lowerRight;
                    indices[counter++] = lowerLeft;

                    indices[counter++] = topLeft;
                    indices[counter++] = topRight;
                    indices[counter++] = lowerRight;
                }
            }
        }

        private void LoadHeightData(Texture2D heightMap)
        {
            terrainWidth = (short)(heightMap.Width);
            terrainHeight = (short)(heightMap.Height);
            if (terrainWidth >= (short)32767)
                terrainWidth = (short)32767;
            if (terrainHeight >= (short)32767)
                terrainHeight = (short)32767;

            Color[] heightMapColors = new Color[terrainWidth * terrainHeight];
            heightMap.GetData(heightMapColors);

            heightData = new float[terrainWidth, terrainHeight];
            for (int x = 0; x < terrainWidth; x++)
                for (int y = 0; y < terrainHeight; y++)
                    heightData[x, y] = heightMapColors[x + y * terrainWidth].R / 5.0f;
        }

        private void CalculateNormals()
        {
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal = new Vector3(0, 0, 0);

            for (int i = 0; i < indices.Length / 3; i++)
            {
                int index1 = indices[i * 3];
                int index2 = indices[i * 3 + 1];
                int index3 = indices[i * 3 + 2];

                Vector3 side1 = vertices[index1].Position - vertices[index3].Position;
                Vector3 side2 = vertices[index1].Position - vertices[index2].Position;
                Vector3 normal = Vector3.Cross(side1, side2);

                vertices[index1].Normal += normal;
                vertices[index2].Normal += normal;
                vertices[index3].Normal += normal;
            }

            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal.Normalize();
        }

    }
}